package edu.mercer.sse.plainpdf;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Level;

import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Predicates;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Range;

import static com.google.common.base.Preconditions.*;
import edu.mercer.sse.annotations.*;
import edu.mercer.sse.configuration.Config;
import edu.mercer.sse.error.CriticalException;
import edu.mercer.sse.error.ReturnCodeException;
import edu.mercer.sse.log.CustomLogger;
import edu.mercer.sse.log.LoggerCustom;

/**
 * Utility: 
 * 
 * Demonstration of by Wrapper Facade pattern.<p>
 * 
 * Compiled application or script executable. Command line parameters used as 
 * 	API: parameters are replaced by variables as specified in Configuration.<p>
 *
 * Class contains shared functions used for parsing and validating XML 
 * 	parameter-variables values, and execution of utilities.<p>
 *
 * Generic X - additional functionality specific to the utility being wrapped.
 * <p>
 * 
 * Example value for 'X':<p> 
 * 
 * <ul><li>Validation&#60;FileDocument&#62;</ul>
 * 
 * AutoCloseable used to shutdown ExecutorService.  If an ExecutorService is
 * 	supplied, shutdown will not be initiated by this class.<p>
 * 
 * @see UtilityWrapping
 * 
 * @see UtilityNomenclature
 * 
 * @see Configuration
 * 
 * @see AutoCloseable
 */

public class Utility<X extends UtilityNomenclature> 

	implements UtilityWrapping<X>, 
	
			   AutoCloseable
{
	/** 
	 * Log for this utility.<p>
	 * 
	 * Configuration VERBOSE setting.<p> 
	 * 
	 * @see LoggerCustom
	 */
	
	CustomLogger _log;
	
	
	/** 
	 * Executor Service for running this command line utility.<p>
	 * 
	 * @see ExecutorService
	 */
	
	ExecutorService _exeService;
	
	
	/** Boolean recording if an external executor service is supplied */
	
	Boolean _externalExeServiceUsage = false;
	
	
	/** 
	 * String representing utility as specified in Configuration; i.e., 
	 * "PDFINFO" 
	 */
	
	String _utilityKey = "";
	
	
	/**
	 * Guava Range&#60;Integer&#62; for acceptable return codes.<p>
	 * 
	 * Return codes configured as acceptable for the utility application to be 
	 * 	executed.<p>
	 * 
	 * @see Range
	 */
	
	Range<Integer> _rcRange = Range.singleton(0);
	
	
	/**
	 * Parameter names and values as specified in Configuration; i.e.,
	 * 'PDFINFO_LOCATION', '/usr/bin/pdfinfo'<p>
	 * 
	 * Contains only parameters for this utility, as specified by utilityKey.
	 * <p>
	 * 
	 * @see #_utilityKey
	 */
	
	private Map<String,String> _parms = Maps.newHashMap();
	
	
	/**
	 * Any variables specified by Configuration.<p>
	 * 
	 * Example excerpt from <code>config.xml</code>:<p>
	 * 
	 * <code>
	 * &#60;entry key="PDFCROP_ARGUMENTS"&#62; --margins {MARGIN_BP} --clip 
	 * 	{FILE_IN} {FILE_OUT} &#60;/entry&#62;
	 * </code>
	 * <p>
	 * 
	 * Example: <code>{FILE_IN}</code>
	 */
	
	private Map<String,String> _commandLineVars = Maps.newHashMap();
	
	
	
	public Utility( final X utilityType )
	{
		final String utilityKey =  utilityType.getUtilityKey();

		checkNotNull( utilityKey, "null utilityKey" );
		
		checkArgument( !utilityKey.isEmpty(), "empty utilityKey" );

		_utilityKey = utilityKey;
		
		_parms = new ParseConfigurationFunction().apply( _utilityKey );
		
		checkArgument( _parms.size() > 0, "no utility configuration found "
				+ "for: " + _utilityKey );

		init();
		
		_exeService = Executors.newSingleThreadExecutor();
		
	}//constructor
	
	
	/**
	 * Constructor.<p>
	 * 
	 * Example <code>config.xml</code> excerpt:<p>
	 * 
	 * <code>&#60;entry key="PDFINFO_ID"&#62; PDFINFO &#60;&#47;entry&#62;
	 * </code>
	 * <p>
	 * 
	 * Example utilityKey value:<p>
	 * 
	 * <ul><li>"PDFINFO"</ul>
	 * 
	 * @see #logInitialization()
	 * 
	 * @see #commandLineVariableDiscovery()
	 * 
	 * @param utilityKey - String representing utility as specified in 
	 * 	Configuration
	 * 
	 * @see Configuration
	 */
	
	public Utility( final String utilityKey )
	{
		checkNotNull( utilityKey, "null utilityKey" );
		
		checkArgument( !utilityKey.isEmpty(), "empty utilityKey" );

		_utilityKey = utilityKey;
		
		_parms = new ParseConfigurationFunction().apply( utilityKey );
		
		checkArgument( _parms.size() > 0, "no utility configuration found "
				+ "for: " + utilityKey );

		init();
		
		_exeService = Executors.newSingleThreadExecutor();
		
	}//constructor

	
	/**
	 * Constructor.<p>
	 * 
	 * Supply external executor service.<p>
	 * 
	 * @param utilityKey - String representing utility as specified in 
	 * 	Configuration
	 * 
	 * @param execServ - ExecutorService
	 * 
	 * @see #Utility(String)
	 * 
	 * @see #logInitialization()
	 * 
	 * @see #commandLineVariableDiscovery()
	 * 
	 * @see Configuration
	 * 
	 * @see ExecutorService
	 */
	
	public Utility( final String utilityKey, final ExecutorService execServ )
	{
		checkNotNull( utilityKey, "null utilityKey" );
		
		checkArgument( !utilityKey.isEmpty(), "empty utilityKey" );

		_utilityKey = utilityKey;
		
		_parms = new ParseConfigurationFunction().apply( utilityKey );
		
		checkArgument( _parms.size() > 0, "no utility configuration found "
				+ "for: " + utilityKey );

		init();

		checkNotNull( execServ, "null Executor Service argument" );
		
		_externalExeServiceUsage = true;
		
		_exeService = execServ;
		
	}//constructor	
	
	
	/**
	 * Initialization tasks.<p>
	 * 
	 * Consolidated logic to reduce logic duplication in overloaded 
	 * 	constructors.<p>
	 * 
	 * @see #logInitialization()
	 * 
	 * @see #commandLineVariableDiscovery()
	 * 
	 * @see #returnCodeRangeInitialization()
	 */
	
	private void init()
	{
		logInitialization();

		commandLineVariableDiscovery();

		returnCodeRangeInitialization();
		
	}//method
	
	
	/**
	 * Define log.<p>
	 * 
	 * Configuration VERBOSE setting enables LogLevel.All.
	 * 
	 * Default is set by LoggerCustom; i.e., Severe.<p>
	 * 
	 * @see Configuration
	 * 
	 * @see LoggerCustom
	 */
	
	private void logInitialization()
	{
		Level logLevel = Level.FINE;
		
		if( Config.factory()
				  .load()
				  .get("VERBOSE")
				  .equalsIgnoreCase("true") )
		{ 
			logLevel = Level.ALL;
			
		}//if

		_log = LoggerCustom.obtainLog( "utility_" + _utilityKey, logLevel );
		
	}//method
	

	/**
	 * Discover any of this utility's command line variables specified in 
	 * configuration.<p> 
	 * 
	 * @see #_commandLineVars
	 */
	
	private void commandLineVariableDiscovery()
	{
		
		if( _parms.containsKey(_utilityKey + "_ARGUMENTS") ) 
		{
			_commandLineVars.putAll( new ParseCommandLineVarsFunction().apply( 
							_parms.get(_utilityKey + "_ARGUMENTS").trim() ));
							
		}//if
		
	}//method
	
	
	/**
	 * Parse configuration for this utility, gathering return code minimum 
	 * 	&amp; maximum.<p>
	 * 
	 * Create Guava Range&#60;Integer&#62; for acceptable return code for this 
	 * 	utility.<p>
	 * 
	 * Example:<p>
	 * 
	 * <code>
	 * &#60;entry key="TIFF2PDF_MIN_RETURN_CODE"&#62; 0 &#60;&#47;entry&#62;
	 * <br>
	 * &#60;entry key="TIFF2PDF_MAX_RETURN_CODE"&#62; 4 &#60;&#47;entry&#62;
	 * </code><p>
	 * 
	 * Return codes may be higher than zero (success) and still be acceptable; 
	 * 	i.e., represent warnings.<p>
	 * 
	 * @see #_rcRange
	 * 
	 * @see Configuration
	 */
	
	private void returnCodeRangeInitialization()
	{
		if( _parms.containsKey( _utilityKey + "_MIN_RETURN_CODE" ) &&
			_parms.containsKey( _utilityKey + "_MAX_RETURN_CODE" ) )
		{
			final Integer minReturnCode = Integer.valueOf( 
					_parms.get( _utilityKey + "_MIN_RETURN_CODE" ));

			final Integer maxReturnCode = Integer.valueOf( 
					_parms.get( _utilityKey + "_MAX_RETURN_CODE" ));
			
			if( maxReturnCode > minReturnCode )
			{
				_rcRange = Range.closed(minReturnCode, maxReturnCode);
			
			}
			else if( maxReturnCode != minReturnCode )
			{	
				_log.warning( _utilityKey + " return code MIN_RETURN_CODE: " 
						+ minReturnCode 
						+ " exceeds MAX_RETURN_CODE: " 
						+ maxReturnCode
						+ "Assuming zero return code required for successful"
						+ " utility command execution.");
			
			}//if
			
		}
		else
		{
			_log.warning( _utilityKey + "return code MIN_RETURN_CODE"
					+ " or MAX_RETURN_CODE not found in configuration."
					+ " Assuming zero return code required for successful"
					+ " utility command execution.");

		}//if
		
	}//method
	

	@Fulfills("UtilityWrapping")
	@Override
	public ImmutableSet<String> getUtilityKeys() 
	{
		return ImmutableSet.copyOf( _parms.keySet() );
		
	}//method

	
	@Fulfills("UtilityWrapping")	
	@Override
	public UtilityWrapping<X> setUtilityValue( 
			
			final String key, 
			
			final String value )
	{
		checkNotNull( key, "null key" );
		
		checkNotNull( value, "null value" );
		
		checkArgument( !key.isEmpty(), "empty key" );
		
		_parms.put( key, value );
		
		checkArgument( _parms.containsKey(key), "failure to verify addition " 
				+ "of " + key + "," + value );
		
		return this;
		
	}//method

	
	@Fulfills("UtilityWrapping")	
	@Override
	public String getUtilityValue( final String key ) 
	{
		checkArgument( _parms.containsKey(key), "key not found: " + key );
		
		return Optional.of( _parms.get( key ) ).or("");

	}//method
	
	
	@Fulfills("UtilityWrapping")
	@Override
	public UtilityWrapping<X> setParameterMap(
			
			final Map<String, String> parameterMap) 
	{
		checkNotNull( parameterMap, "null parameterMap" );

		checkArgument( parameterMap.size() > 0, "empty parameterMap" );
		
		_parms.clear();
		
		_parms.putAll( parameterMap ); 

		checkArgument( _parms.size() == parameterMap.size(), "failed to add "
				+ "parameters, size unequal to supplied parameterMap" );
		
		return this;

	}//method

	
	@Fulfills("UtilityWrapping")
	@Override
	public ImmutableMap<String, String> getParameterMap() 
	{
		return ImmutableMap.copyOf(_parms);
		
	}//method


	@Fulfills("UtilityWrapping")
	@Override
	public Boolean isReady() 
	{
		if( _commandLineVars.isEmpty() ||
			
			/* 
			 * ensure any command line variables discovered have been supplied 
			 * with values
			 */
				
			Maps.filterValues( _commandLineVars, new EmptyValuePredicate())
			.isEmpty())
		{
			return true;
			
		}//if
		
		System.out.println(	Maps.filterValues( _commandLineVars, new EmptyValuePredicate())
				);

		
		return false;

	}//method

	
	@Fulfills("UtilityWrapping")
	@Override
	public Set<String> getCommandLineVarKeys() 
	{
		return _commandLineVars.isEmpty() ? 
				
				new ImmutableSet.Builder<String>().build() : 
					
				ImmutableSet.copyOf( _commandLineVars.keySet() );
		
	}//method

	
	@Fulfills("UtilityWrapping")
	@Override
	public UtilityWrapping<X> setCommandLineVarValue(
			
			final String key, final String value) 
	{
		checkNotNull( key, 
				
					  "null command line variable key" );
		
		checkNotNull( value, 
				
					  "null command line variable value" );
		
		checkArgument( !key.isEmpty(),
				
					   "empty command line variable key" );
		
		checkArgument( !value.isEmpty(), 
				
					   "empty command line variable value" );
		
		checkArgument( _commandLineVars.containsKey(key), 
				
					   "key not found: " + key );

		_commandLineVars.put( key, value );
		
		checkArgument( _commandLineVars.get(key).equals(value), 
				
				"failed to verify insertion value: " + key + "," + value );
		
		return this;
	
	}//method

	
	@Fulfills("UtilityWrapping")
	@Override
	public String getCommandLineVarValue(String key) 
	{
		checkNotNull( key, 
				
				  "null command line variable key" );

		checkArgument( !key.isEmpty(),
				
				   "empty command line variable key" );
		
		checkArgument( _commandLineVars.containsKey(key), 
				
				   "key not found: " + key );
		
		return Optional.of(_commandLineVars.get(key)).or("");
		
	}//method

	
	@Fulfills("UtilityWrapping")
	@Override
	public UtilityWrapping<X> setCommandLineVarMap(
			
			final Map<String, String> commandLineVarMap ) 
	{
		checkNotNull( commandLineVarMap, 
				
				"null command line variable map" );
		
		checkArgument( !commandLineVarMap.isEmpty(), 
				
				"empty command line variable map" );

		_commandLineVars.clear();
		
		_commandLineVars.putAll( commandLineVarMap );
		
		checkArgument( _commandLineVars.size() == commandLineVarMap.size(),
				
				"size unexpected, replacement of command line variables Map"
				+ " verification failure for: " + commandLineVarMap  );
		
		return this;
		
	}//method

	
	@Fulfills("UtilityWrapping")
	@Override
	public Map<String, String> getCommandLineVarMap() 
	{
		return ImmutableMap.copyOf( _commandLineVars );
		
	}//method

	
	public Future<ExecutionReporting> asyncInvoke()
	{
		return _exeService.submit(this);
		
	}//method
	
	
	@Fulfills("UtilityWrapping")
	@Override
	public ExecutionReporting call() throws ReturnCodeException
	{
		ExecutionReporting taskReport = new ExecutionReport();
		
		try
		{
			final String commandLine = _parms.get( _utilityKey 
					+ "_LOCATION" );

			String commandLineArgs = _parms.get( _utilityKey 
					+ "_ARGUMENTS" );

			final Iterator<String> cmdLineVarIter = getCommandLineVarMap()
					.keySet()
					.iterator();

			_log.fine("command line variables and values: " 
					+ getCommandLineVarMap());
			
			while( cmdLineVarIter.hasNext() )
			{
				String keyCurrent = cmdLineVarIter.next();

				String valCurrent = _commandLineVars.get( keyCurrent );

				_log.fine("command line variables: " + keyCurrent 
						+ " replacement with: " 
						+ valCurrent );
				
				commandLineArgs = commandLineArgs.replace( 
						
						"{" + keyCurrent + "}", 
						
						valCurrent );

			}//while

			/* 
			 * check for any '{' or '}' which should have been switch, as 
			 * '{FILE_IN}' becomes 'example.pdf' 
			 */
			
			if( commandLineArgs.matches("[^\\}\\}]{0,}"
									  
									  + "[\\}|\\{]"
									  
									  + "[^\\}\\}]{0,}"))
			{
				_log.warning("command line arg substitution may have failed: " 
						+ commandLineArgs);
				
			}//if
			
			taskReport = runSystemCommand( (commandLine 
							+ " " 
							+ commandLineArgs).split("\\s") );
		
			if( !_rcRange.contains( taskReport.getReturnCode() ) )
			{
				throw new ReturnCodeException( _utilityKey + ":" 
						+ taskReport.getReturnCode() 
						+ "," 
						+ taskReport );
				
			}//if

		}
		catch( final Exception err )
		{
			/* wrap unknown exceptions to comply with call */
			
			throw new CriticalException(err);
		
		}//try
				
		return taskReport;
		
	}//method


	@Fulfills("AutoCloseable")
	@Override
	public void close() 
	{
		if( !_externalExeServiceUsage )
		{
			new ExecutorServiceTerminationFunction().apply( _exeService );
			
		}//if
		
	}//method

	
	/**
	 * Run command-line, external application.<p>
	 * 
	 * @param cmd - String[] array containing space-delimited command and its 
	 * 	arguments
	 * 
	 * @return  ExecutionReporting - contains output, results of command line 
	 * 	application utility execution
	 * 
	 * @throws IOException - error running application
	 * 
	 * @throws InterruptedException - thread timeout or shutdown
	 * 
	 * @see ExecutionReporting
	 * 
	 * @see ProcessBuilder
	 */
	
	private ExecutionReporting runSystemCommand( final String[] cmd ) throws 
	
		IOException,
		
		InterruptedException
	{
		ArrayList<String> cmdList = Lists.newArrayList( cmd );
		
		ArrayList<String> cmdListClean = Lists.newArrayList( 
				
				Collections2.filter(cmdList, 
				
				Predicates.not(new LineContinuationPredicate())));
		
		final ProcessBuilder probuilder = new ProcessBuilder( 
				
				cmdListClean.toArray(new String[]{}) );

		final Process process = probuilder.start();

		final InputStream inStream = process.getInputStream();
		final InputStreamReader inStreamRdr = new InputStreamReader(inStream);
		final BufferedReader br = new BufferedReader(inStreamRdr);

		final StringBuilder resultsTotal = new StringBuilder();

		String results;

		while( (results = br.readLine()) != null ) 
		{
			resultsTotal.append( results );
			resultsTotal.append( "\n" );

		}//while

		_log.fine( "command execution: " + Joiner.on(" ").join(cmd) );
		
		return new ExecutionReport()
		
			.setReturnCode( process.waitFor() )

			.setOutput( resultsTotal.toString() );

	}//method	
	
	
}//class
